package errors

import (
	errors2 "errors"
	"fmt"
	"net/http"

	"gitee.com/zawei/yggdrasil/pkg"
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/ptypes"
	"github.com/pkg/errors"
	"google.golang.org/genproto/googleapis/rpc/code"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	spb "google.golang.org/genproto/googleapis/rpc/status"
)

const (
	ErrorPackageIsVersion1 = true
)

type statusError spb.Status

func (se *statusError) Error() string {
	p := (*spb.Status)(se)
	return fmt.Sprintf("rpc error: code = %s desc = %s", code.Code(p.GetCode()), p.GetMessage())
}

func (se *statusError) Status() *Status {
	return &Status{s: (*spb.Status)(se)}
}

// Status statusError is an alias of a status proto
// implement ecode.Codes
type Status struct {
	s *spb.Status
}

// Code returns the status code contained in s.
func (s *Status) Code() code.Code {
	if s == nil || s.s == nil {
		return code.Code_OK
	}
	return code.Code(s.s.Code)
}

// Code returns the http status code contained in s.
func (s *Status) HttpCode() int32 {
	if s == nil || s.s == nil {
		return http.StatusOK
	}
	return CodeToHttpStatus(s.Code())
}

// Message returns the message contained in s.
func (s *Status) Message() string {
	if s == nil || s.s == nil {
		return ""
	}
	return s.s.Message
}

// Proto returns s's status as an spb.Status proto message.
func (s *Status) Proto() *spb.Status {
	if s == nil {
		return nil
	}
	return proto.Clone(s.s).(*spb.Status)
}

// Err returns an immutable error representing s; returns nil if s.Code() is
// OK.
func (s *Status) Err() error {
	if s.Code() == code.Code_OK {
		return nil
	}
	return (*statusError)(s.s)
}

// WithDetails returns a new status with the provided details messages appended to the status.
func (s *Status) WithDetails(details ...proto.Message) *Status {
	if s.Code() == code.Code_OK {
		return s
	}
	// s.Code() != OK implies that s.Proto() != nil.
	p := s.Proto()
	for _, detail := range details {
		any, err := ptypes.MarshalAny(detail)
		if err != nil {
			continue
		}
		p.Details = append(p.Details, any)
	}
	return &Status{s: p}
}

// WithReason returns a new status with the error details messages appended to the status.
func (s *Status) WithErrInfo(reason string, kvs ...string) *Status {
	if s.Code() == code.Code_OK {
		return s
	}

	if len(kvs)%2 == 1 {
		panic("got the odd number of input kvs")
	}
	md := map[string]string{}
	var key string
	for i, s := range kvs {
		if i%2 == 0 {
			key = s
			continue
		}
		md[key] = s
	}
	return s.WithDetails(&errdetails.ErrorInfo{
		Reason:   reason,
		Domain:   pkg.AppName(),
		Metadata: md,
	})
}

// Details returns a slice of details messages attached to the status.
// If a detail cannot be decoded, the error is returned in place of the detail.
func (s *Status) Details() []interface{} {
	if s == nil || s.s == nil {
		return nil
	}
	details := make([]interface{}, 0, len(s.s.Details))
	for _, any := range s.s.Details {
		detail := &ptypes.DynamicAny{}
		if err := ptypes.UnmarshalAny(any, detail); err != nil {
			details = append(details, err)
			continue
		}
		details = append(details, detail.Message)
	}
	return details
}

// FromError convert error for service reply and try to convert it to grpc.Status.
func FromError(err error) (gst *Status) {
	if err == nil {
		return &Status{s: &spb.Status{Code: 0, Message: "OK"}}
	}
	err = errors.Cause(err)
	se := new(statusError)
	if !errors2.As(err, &se) {
		return Unknown(err.Error())
	}
	return se.Status()
}

// FromProto returns a Status representing s.
func FromProto(s *spb.Status) *Status {
	return &Status{s: proto.Clone(s).(*spb.Status)}
}

// FromProto returns the reason of error.
func Reason(err error) string {
	for _, detail := range FromError(err).Details() {
		if info, ok := detail.(*errdetails.ErrorInfo); ok {
			return info.Reason
		}
	}
	return ""
}

func CodeToHttpStatus(c code.Code) int32 {
	switch c {
	case code.Code_OK:
		return http.StatusOK
	case code.Code_CANCELLED:
		return 499
	case code.Code_UNKNOWN:
		return http.StatusInternalServerError
	case code.Code_INVALID_ARGUMENT:
		return http.StatusBadRequest
	case code.Code_DEADLINE_EXCEEDED:
		return http.StatusGatewayTimeout
	case code.Code_NOT_FOUND:
		return http.StatusNotFound
	case code.Code_ALREADY_EXISTS:
		return http.StatusConflict
	case code.Code_PERMISSION_DENIED:
		return http.StatusForbidden
	case code.Code_UNAUTHENTICATED:
		return http.StatusUnauthorized
	case code.Code_RESOURCE_EXHAUSTED:
		return http.StatusTooManyRequests
	case code.Code_FAILED_PRECONDITION:
		return http.StatusOK
	case code.Code_ABORTED:
		return http.StatusConflict
	case code.Code_OUT_OF_RANGE:
		return http.StatusBadRequest
	case code.Code_UNIMPLEMENTED:
		return http.StatusNotImplemented
	case code.Code_INTERNAL:
		return http.StatusInternalServerError
	case code.Code_UNAVAILABLE:
		return http.StatusServiceUnavailable
	case code.Code_DATA_LOSS:
		return http.StatusInternalServerError
	default:
		return http.StatusInternalServerError
	}
}
